package com.binance.basedao.mybatis;

import com.binance.basedao.anotation.Id;
import com.binance.basedao.anotation.Table;
import com.binance.basedao.complexquery.CustomQueryParam;
import com.binance.basedao.complexquery.NoValueQueryParam;
import com.binance.basedao.complexquery.WithValueQueryParam;
import com.binance.basedao.enums.IDGenerateType;
import org.apache.ibatis.jdbc.SQL;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <b>Description:</b>〈BaseDao中常用查询的具体实现〉<br/>
 * @author hongchaoMao <br/>
 *         Create date: 2017/9/18
 * @version v1.0.0
 */
public class BaseProvider<T>
{
    private final Logger logger = Logger.getLogger(getClass().getName());
    private String   tableName;
    private Class<?> modelClass;
    private static KeyGenerator keyGenerator = KeyGenerator.getInstance();
    private static ThreadLocal<Class<?>> threadModelClass = new ThreadLocal<>();
    private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final String                OPERATOR_EQUAL   = " = ";
    private static final String                OPERATOR_LIKE    = " like ";

    private void initFromThreadLocal()
    {
        modelClass = BaseProvider.threadModelClass.get();
        if (modelClass == null) {
            throw new RuntimeException("The model class set failed !");
        }
        tableName = modelClass.getAnnotation(Table.class).name();
    }

    static void setModelClass(Class<?> modelClass)
    {
        BaseProvider.threadModelClass.set(modelClass);
    }

    static void threadLocalRemove()
    {
        if (BaseProvider.threadModelClass != null){
            BaseProvider.threadModelClass.remove();
        }
    }

    public String getAll()
    {
        initFromThreadLocal();
        SQL sql = selectFrom();
        return ORDER(null, sql).toString();
    }

    public String getById()
    {
        initFromThreadLocal();
        Map<String, Property> columns = ModelUtils.getProperties(modelClass, ColumnTarget.SELECT);
        SQL sql = selectFrom(columns);
        byId(sql);
        return sql.toString();
    }

    private String commonCount(T findParams, String operatorEqual) throws IllegalAccessException, InvocationTargetException {
        initFromThreadLocal();
        SQL sql = new SQL();
        sql.SELECT("COUNT(ID)").FROM(tableName);
        return where(findParams, sql, operatorEqual).toString();
    }

    public String count(T findParams) throws IllegalAccessException, InvocationTargetException
    {
        return commonCount(findParams, OPERATOR_EQUAL);
    }

    public String countLike(T findParams) throws IllegalAccessException, InvocationTargetException
    {
        return commonCount(findParams, OPERATOR_LIKE);
    }

    public String countQuery(Map<String, Object> customQueryParams)
    {
        initFromThreadLocal();
        SQL sql = new SQL();
        sql.SELECT("COUNT(ID)").FROM(tableName);
        return whereCustom(customQueryParams, sql).toString();
    }

    @SuppressWarnings("unchecked")
    public String query(Map<String, Object> dataMap)
    {
        List<Sort> sortList = (List<Sort>) dataMap.get("sortList");
        initFromThreadLocal();
        SQL sql = selectFrom();
        whereCustom(dataMap, sql);
        if (sortList != null && !sortList.isEmpty()) {
            ORDER(sortList, sql);
        }
        return sql.toString();
    }

    public String find(T findParams) throws IllegalAccessException, InvocationTargetException
    {
        initFromThreadLocal();
        SQL sql = selectFrom();
        return where(findParams, sql, OPERATOR_LIKE).toString();
    }

    public String get(T findParams) throws IllegalAccessException, InvocationTargetException
    {
        initFromThreadLocal();
        SQL sql = selectFrom();
        return where(findParams, sql, OPERATOR_EQUAL).toString();
    }

    @SuppressWarnings("unchecked")
    public String enhanceGet(Map<String, Object> dataMap) throws IllegalAccessException, InvocationTargetException
    {
        List<Sort> sortList = (List<Sort>) dataMap.get("sortList");
        initFromThreadLocal();
        SQL sql = selectFrom();
        where(dataMap, sql);
        if (sortList != null && !sortList.isEmpty()) {
            ORDER(sortList, sql);
        }
        return sql.toString();
    }

    @SuppressWarnings("unchecked")
    public String insert(Map<String, Object> dataMap) throws IllegalAccessException, InvocationTargetException
    {
        initFromThreadLocal();
        final T model = (T) dataMap.get("model");
        Boolean ignore = (Boolean)dataMap.get("ignore");
        dataMap.clear();

        setIdValue(model);
        SQL sql = new SQL();
        sql.INSERT_INTO(tableName);
        Map<String, Property> properties = ModelUtils.getProperties(model, ColumnTarget.INSERT);
        for (Property property : properties.values()) {
            if (property.isNullValue(model)) {
                continue;
            }
            dataMap.put(property.getName(),property.getValue(model));
            sql.VALUES(property.getColumnName(), "#{" + property.getName() + "}");
        }
        if (ignore){
            return sql.toString().replaceFirst("INSERT INTO","INSERT IGNORE INTO");
        }
        return sql.toString();
    }

    private void setIdValue(T t) {
        Class<?> clazz = t.getClass();
        Field[] fields = clazz.getDeclaredFields();
        try {
            for (Field field: fields) {
                Id idAnnotaion = field.getAnnotation(Id.class);
                if (idAnnotaion != null) {
                    String name = field.getName();
                    String wirteMethod = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
                    String readMethod = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
                    Method read = clazz.getMethod(readMethod);
                    Object val = null;
                    if (read != null) {
                        read.setAccessible(true);
                        val = read.invoke(t);
                    }
                    // 没有值才进行生成
                    if (val == null) {
                        IDGenerateType type = idAnnotaion.generateBy();
                        if (IDGenerateType.APP.equals(type)) {
                            Method write = clazz.getMethod(wirteMethod, field.getType());
                            if (write != null) {
                                write.setAccessible(true);
                                write.invoke(t, keyGenerator.generateKey());
                            }
                        }
                    }
                    break;
                }
            }
        } catch (Exception ex) {
            logger.log(Level.WARNING, "Error occurred when automatic generate the primary id -> " + ex);
        }
    }

    @SuppressWarnings("unchecked")
    public String insertBatch(final Map<String, Object> param) throws IllegalAccessException, InvocationTargetException
    {
        initFromThreadLocal();
        List<T> list = (List<T>)param.get("list");
        Boolean ignore = (Boolean)param.get("ignore");
        param.clear();

        T cla = list.get(0);
        Class<?> classType = cla.getClass();

        Map<String, Property> properties = ModelUtils.getProperties(cla, null);
        SQL sql = new SQL();
        sql.INSERT_INTO(tableName);
        for (Property property : properties.values()) {
            sql.VALUES(property.getColumnName(), "#{" + property.getName() + "}");
        }
        Method[] methods = classType.getMethods();

        Map<String, Method> methodMap = new HashMap<>();
        for (Method method : methods) {
            methodMap.put(method.getName(), method);
        }
        Map<String, Class> typeMap = new HashMap<>();
        Map<String, Field> fieldMap = new HashMap<>();
        try {
            Field[] declaredFields = classType.getDeclaredFields();
            for (Field field : declaredFields) {
                if (!fieldMap.containsKey(field.getName())) {
                    fieldMap.put(field.getName(), field);
                    typeMap.put(field.getName(), field.getType());
                }
            }
        }
        catch (SecurityException ex) {
            logger.log(Level.WARNING, "Get class declared fields error: " + ex);
        }
        String sqlStr = sql.toString();
        int index = sqlStr.lastIndexOf('(');
        String prefixSql = sqlStr.substring(0, index);
        StringBuilder sqlBuffer = new StringBuilder(prefixSql);
        if (ignore){
            sqlBuffer.replace(0,11,"INSERT IGNORE INTO");
        }
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
            setIdValue(t);
            if (i == 0) {
                sqlBuffer.append("(");
            }
            else {
                sqlBuffer.append(",(");
            }
            Iterator<Property> iterator = properties.values().iterator();
            while (iterator.hasNext()) {
                Property property = iterator.next();

                String fieldName = property.getName();
                String methodName;
                if (fieldName.length() == 1) {
                    methodName = fieldName.substring(0, 1).toUpperCase();
                }
                else {
                    methodName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1, fieldName.length());
                }
                methodName = "get" + methodName;
                Method method = methodMap.get(methodName);
                method.setAccessible(true);
                Object val = method.invoke(t);
                Class type = typeMap.get(fieldName);

                if (type.isEnum()) {
                    sqlBuffer.append("'");
                    if (val != null) {
                        Enum<?> e = (Enum<?>) val;
                        sqlBuffer.append(e.name());
                    }
                    sqlBuffer.append("'");
                }
                else if (Date.class.equals(type)) {
                    if (val != null) {
                        sqlBuffer.append("'");
                        Date date = (Date) val;
                        LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
                        sqlBuffer.append(localDateTime.format(dateFormatter));
                        sqlBuffer.append("'");
                    }
                    else {
                        sqlBuffer.append("null");
                    }

                }
                else {
                    if (val != null) {
                        sqlBuffer.append("'");
                        sqlBuffer.append(val.toString());
                        sqlBuffer.append("'");
                    }else {
                        sqlBuffer.append("null");
                    }
                }
                if (iterator.hasNext()) {
                    sqlBuffer.append(",");
                }
            }
            sqlBuffer.append(")");
        }
        return sqlBuffer.toString();
    }

    public String deleteById(Long id)
    {
        initFromThreadLocal();
        logger.log(Level.INFO, "Delete by primary key, id = " + id);
        SQL sql = new SQL();
        sql.DELETE_FROM(tableName);
        byId(sql);
        return sql.toString();
    }

    private void byId(SQL sql) {
        Map<String, Property> columns = ModelUtils.getProperties(modelClass, ColumnTarget.SELECT);
        boolean notIdAnotation = true;
        Iterator<String> keyIterator = columns.keySet().iterator();
        while (keyIterator.hasNext()) {
            String columName = keyIterator.next();
            if (columns.get(columName).isId()) {
                sql.WHERE(columName +" = #{" + columName + "}");
                notIdAnotation = false;
                break;
            }
        }
        if (notIdAnotation) {
            sql.WHERE("ID = #{id}");
        }
    }

    public String deleteByPrimaryKey(final T t) throws IllegalAccessException, InvocationTargetException, NoSuchFieldException
    {
        initFromThreadLocal();
        boolean flg = false;
        SQL sql = new SQL();
        sql.DELETE_FROM(tableName);
        Map<String, Property> properties = ModelUtils.getProperties(t, null);
        for (Property property : properties.values()) {
            if (property.isId()) {
                sql.WHERE(property.getColumnName() + " = #{" + property.getName() + "}");
                flg = true;
            }
        }
        if (!flg) {
            throw new UnsupportedOperationException("Entity's primary field without @Id annotation .");
        }
        logger.log(Level.INFO, "Delete by primary key, sql = " + sql.toString());
        return sql.toString();
    }

    public String update(final T t) throws IllegalAccessException, InvocationTargetException
    {
        initFromThreadLocal();
        try {
            Method method = t.getClass().getMethod("setUpdateTime", Date.class);
            if (method != null) {
                method.setAccessible(true);
                method.invoke(t, new Date());
            }
        }catch (Exception e) {
            logger.log(Level.WARNING, "------>update() Class:" + t.getClass().getName() + ",not found update_time .");
        }
        SQL sql = new SQL();
        sql.UPDATE(tableName);
        Property primaryCol = null;
        String className = modelClass.getName().split("$")[0];
        try {
            Map<String, Property> properties = ModelUtils.getProperties(Class.forName(className), ColumnTarget.UPDATE);

            for (Property property : properties.values()) {
                // 第一个@Id作为update条件
                if (property.isId() && primaryCol == null){
                    primaryCol = property;
                    continue;
                } // 过滤不允许更新的字段
                else if (property.isNullValue(t) || property.isId()) {
                    continue;
                } // 创建时间不可更新，create_time
                else if ("create_time".equalsIgnoreCase(property.getColumnName())){
                    continue;
                }
                sql.SET(property.getColumnName() + " = #{" + property.getName() + "}");
            }
        }
        catch (ClassNotFoundException e) {
            logger.log(Level.WARNING, className + " not found!");
        }
        if (primaryCol != null){
            sql.WHERE(primaryCol.getColumnName() + "= #{" + primaryCol.getName() + "}");
        }else {
            sql.WHERE("ID = #{id}");
        }
        return sql.toString();
    }

    private SQL selectFrom() {
        Map<String, Property> columns = ModelUtils.getProperties(modelClass, ColumnTarget.SELECT);
        return selectFrom(columns);
    }

    private SQL selectFrom(Map<String, Property> columns)
    {
        SQL sql = new SQL();
        for (Property property : columns.values()) {
            sql.SELECT(property.getColumnName());
        }
        sql.FROM(tableName);
        return sql;
    }


    private SQL where(T findParams, SQL sql, String operator) throws IllegalAccessException, InvocationTargetException
    {
        Map<String, Property> properties = ModelUtils.getProperties(findParams, ColumnTarget.WHERE);

        for (Property property : properties.values()) {
            // null或者空白字符串的不作为查询条件
            Object value = property.getValue(findParams);
            if (value == null || "".equals(value.toString().trim())){
                continue;
            }
            sql.WHERE(property.getColumnName() + operator + "#{" + property.getName() + "}");
        }
        return sql;
    }

    @SuppressWarnings("unchecked")
    private SQL where(Map<String, Object> dataMap, SQL sql) throws IllegalAccessException, InvocationTargetException
    {
        T findParams = (T)dataMap.get("findParams");
        dataMap.clear();

        Map<String, Property> properties = ModelUtils.getProperties(findParams, ColumnTarget.WHERE);
        for (Property property : properties.values()) {
            // null或者空白字符串的不作为查询条件
            Object value = property.getValue(findParams);
            if (value == null || "".equals(value.toString().trim())){
                continue;
            }
            dataMap.put(property.getName(), value);
            sql.WHERE(property.getColumnName() + OPERATOR_EQUAL + "#{" + property.getName() + "}");
        }
        return sql;
    }

    @SuppressWarnings("unchecked")
    private SQL whereCustom(Map<String, Object> dataMap, SQL sql)
    {
        Map<String, Property> properties = ModelUtils.getProperties(modelClass, null);
        List<CustomQueryParam> customQueryParams = (List<CustomQueryParam>) dataMap.get("queryParams");
        dataMap.clear();

        if (customQueryParams == null) {
            return sql;
        }
        for (CustomQueryParam customQueryParam : customQueryParams) {
            String key = customQueryParam.getProperty();
            Property property = properties.get(key);
            if (property != null) {
                if (customQueryParam instanceof WithValueQueryParam) {
                    WithValueQueryParam withValueQueryParam = (WithValueQueryParam) customQueryParam;
                    Object value = dataMap.putIfAbsent(key, withValueQueryParam.getValue());
                    if (value != null){ // 针对时间范围查询的问题
                        key += "_2";
                        dataMap.put(key, withValueQueryParam.getValue());
                    }
                    sql.WHERE(property.getColumnName() + " " + withValueQueryParam.getOperator() + " #{" + key + "}");
                } else if (customQueryParam instanceof NoValueQueryParam) {
                    NoValueQueryParam noValueQueryParam = (NoValueQueryParam) customQueryParam;
                    sql.WHERE(property.getColumnName() + " " + noValueQueryParam.getCondition());
                }
            }
        }
        return sql;
    }

    private SQL ORDER(List<Sort> sortList, SQL sql)
    {
        Map<String, Property> properties = ModelUtils.getProperties(modelClass, ColumnTarget.ORDER);
        for (Property property : properties.values()) {
            sql.ORDER_BY(property.getOrder());
        }
        if (sortList != null) {
            for (Sort sort : sortList) {
                sql.ORDER_BY(sort.getColumn() + " " + sort.getDirection());
            }
        }
        return sql;
    }

}
